#include <fc/crypto/elliptic.hpp>

#include <fc/crypto/base58.hpp>
#include <fc/crypto/openssl.hpp>

#include <fc/fwd_impl.hpp>
#include <fc/exception/exception.hpp>
#include <fc/log/logger.hpp>

#include "_elliptic_impl_pub.hpp"

namespace fc { namespace ecc {
namespace detail {
void
_init_lib() {
    static int init_o = init_openssl();
}

class private_key_impl {
public:
    private_key_impl() BOOST_NOEXCEPT {
        _init_lib();
    }

    private_key_impl(const private_key_impl& cpy) BOOST_NOEXCEPT {
        _init_lib();
        *this = cpy;
    }

    private_key_impl(private_key_impl&& cpy) BOOST_NOEXCEPT {
        _init_lib();
        *this = cpy;
    }

    ~private_key_impl() BOOST_NOEXCEPT {
        free_key();
    }

    private_key_impl& operator=(const private_key_impl& pk) BOOST_NOEXCEPT {
        if(pk._key == nullptr) {
            free_key();
        }
        else if(_key == nullptr) {
            _key = EC_KEY_dup(pk._key);
        }
        else {
            EC_KEY_copy(_key, pk._key);
        }
        return *this;
    }

    private_key_impl& operator=(private_key_impl&& pk) BOOST_NOEXCEPT {
        if(this != &pk) {
            free_key();
            _key    = pk._key;
            pk._key = nullptr;
        }
        return *this;
    }

    EC_KEY* _key = nullptr;

private:
    void free_key() BOOST_NOEXCEPT {
        if(_key != nullptr) {
            EC_KEY_free(_key);
            _key = nullptr;
        }
    }
};
}  // namespace detail

private_key::private_key() {}

private_key::private_key(const private_key& pk)
    : my(pk.my) {}

private_key::private_key(private_key&& pk)
    : my(std::move(pk.my)) {}

private_key::~private_key() {}

private_key&
private_key::operator=(private_key&& pk) {
    my = std::move(pk.my);
    return *this;
}

private_key&
private_key::operator=(const private_key& pk) {
    my = pk.my;
    return *this;
}
static void*
ecies_key_derivation(const void* input, size_t ilen, void* output, size_t* olen) {
    if(*olen < SHA512_DIGEST_LENGTH) {
        return NULL;
    }
    *olen = SHA512_DIGEST_LENGTH;
    return (void*)SHA512((const unsigned char*)input, ilen, (unsigned char*)output);
}

int static inline EC_KEY_regenerate_key(EC_KEY* eckey, const BIGNUM* priv_key) {
    int       ok      = 0;
    BN_CTX*   ctx     = NULL;
    EC_POINT* pub_key = NULL;

    if(!eckey)
        return 0;

    const EC_GROUP* group = EC_KEY_get0_group(eckey);

    if((ctx = BN_CTX_new()) == NULL)
        goto err;

    pub_key = EC_POINT_new(group);

    if(pub_key == NULL)
        goto err;

    if(!EC_POINT_mul(group, pub_key, priv_key, NULL, NULL, ctx))
        goto err;

    EC_KEY_set_private_key(eckey, priv_key);
    EC_KEY_set_public_key(eckey, pub_key);

    ok = 1;

err:

    if(pub_key)
        EC_POINT_free(pub_key);
    if(ctx != NULL)
        BN_CTX_free(ctx);

    return (ok);
}

private_key
private_key::regenerate(const fc::sha256& secret) {
    private_key self;
    self.my->_key = EC_KEY_new_by_curve_name(NID_secp256k1);
    if(!self.my->_key)
        FC_THROW_EXCEPTION(exception, "Unable to generate EC key");

    ssl_bignum bn;
    BN_bin2bn((const unsigned char*)&secret, 32, bn);

    if(!EC_KEY_regenerate_key(self.my->_key, bn)) {
        FC_THROW_EXCEPTION(exception, "unable to regenerate key");
    }
    return self;
}

fc::sha256
private_key::get_secret() const {
    return get_secret(my->_key);
}

private_key::private_key(EC_KEY* k) {
    my->_key = k;
}

public_key
private_key::get_public_key() const {
    public_key pub;
    pub.my->_key = EC_KEY_new_by_curve_name(NID_secp256k1);
    EC_KEY_set_public_key(pub.my->_key, EC_KEY_get0_public_key(my->_key));
    return pub;
}

fc::sha512
private_key::get_shared_secret(const public_key& other) const {
    FC_ASSERT(my->_key != nullptr);
    FC_ASSERT(other.my->_key != nullptr);
    fc::sha512 buf;
    ECDH_compute_key((unsigned char*)&buf, sizeof(buf), EC_KEY_get0_public_key(other.my->_key), my->_key, ecies_key_derivation);
    return buf;
}

compact_signature
private_key::sign_compact(const fc::sha256& digest, bool require_canonical) const {
    try {
        FC_ASSERT(my->_key != nullptr);
        auto my_pub_key = get_public_key().serialize();  // just for good measure
        //ECDSA_SIG *sig = ECDSA_do_sign((unsigned char*)&digest, sizeof(digest), my->_key);
        public_key_data key_data;
        while(true) {
            ecdsa_sig sig = ECDSA_do_sign((unsigned char*)&digest, sizeof(digest), my->_key);

            if(sig == nullptr)
                FC_THROW_EXCEPTION(exception, "Unable to sign");

            compact_signature csig;
            // memset( csig.data, 0, sizeof(csig) );

            int nBitsR = BN_num_bits(sig->r);
            int nBitsS = BN_num_bits(sig->s);
            if(nBitsR <= 256 && nBitsS <= 256) {
                int     nRecId = -1;
                EC_KEY* key    = EC_KEY_new_by_curve_name(NID_secp256k1);
                FC_ASSERT(key);
                EC_KEY_set_conv_form(key, POINT_CONVERSION_COMPRESSED);
                for(int i = 0; i < 4; i++) {
                    if(detail::public_key_impl::ECDSA_SIG_recover_key_GFp(key, sig, (unsigned char*)&digest, sizeof(digest), i, 1) == 1) {
                        unsigned char* buffer = (unsigned char*)key_data.begin();
                        i2o_ECPublicKey(key, &buffer);  // FIXME: questionable memory handling
                        if(key_data == my_pub_key) {
                            nRecId = i;
                            break;
                        }
                    }
                }
                EC_KEY_free(key);

                if(nRecId == -1) {
                    FC_THROW_EXCEPTION(exception, "unable to construct recoverable key");
                }
                unsigned char* result = nullptr;
                auto           bytes  = i2d_ECDSA_SIG(sig, &result);
                auto           lenR   = result[3];
                auto           lenS   = result[5 + lenR];
                //idump( (result[0])(result[1])(result[2])(result[3])(result[3+lenR])(result[4+lenR])(bytes)(lenR)(lenS) );
                if(lenR != 32) {
                    free(result);
                    continue;
                }
                if(lenS != 32) {
                    free(result);
                    continue;
                }
                //idump( (33-(nBitsR+7)/8) );
                //idump( (65-(nBitsS+7)/8) );
                //idump( (sizeof(csig) ) );
                memcpy(&csig.data[1], &result[4], lenR);
                memcpy(&csig.data[33], &result[6 + lenR], lenS);
                //idump( (csig.data[33]) );
                //idump( (csig.data[1]) );
                free(result);
                //idump( (nRecId) );
                csig.data[0] = nRecId + 27 + 4;  //(fCompressedPubKey ? 4 : 0);
                /*
                    idump( (csig) );
                    auto rlen = BN_bn2bin(sig->r,&csig.data[33-(nBitsR+7)/8]);
                    auto slen = BN_bn2bin(sig->s,&csig.data[65-(nBitsS+7)/8]);
                    idump( (rlen)(slen) );
                    */
            }
            return csig;
        }  // while true
    }
    FC_RETHROW_EXCEPTIONS(warn, "sign ${digest}", ("digest", digest)("private_key", *this));
}
}}  // namespace fc::ecc
